
require "Json"

-- {{{ import
local string = string
local table = table
local coroutine = coroutine
local debug = debug
local io = io
local os = os
local pairs = pairs
local ipairs = ipairs
local print = print
local require = require
local error = error
local type = type
local tostring = tostring
local pcall = pcall
local assert = assert
local loadfile = loadfile
local unpack = unpack
local select = select

local Json = Json

local G = _G
-- }}}

module("toydebugger")

-- {{{ Variables
local GlobalNames = {}

local sock
local BreakPoints = {}
local Hooking
local Threads
local ThreadDepth
local Mode = 'si'
local Protocol = 'json'
-- }}}

-- {{{ Hook
function IsInDebug()
	local X = debug.getinfo('3', 'S')
	if string.find(X.short_src, 'toydebugger.lua') then
		return true
	else
		return false
	end
end

function IsInBreak(Line)
	if table.maxn(BreakPoints) == 0  or not Line then
		return false
	end
	local File
	for I, BP in ipairs(BreakPoints) do
		if BP.Line == Line then
			if not File then
				local X = debug.getinfo('3', 'Sl')
				File = X.source
			end
			if BP.File == File then
				return true
			end
		end
	end
	return false
end

function GetDepth()
	local Depth = 1
	for Info in
		function()
			return debug.getinfo(Depth)
		end
	do
		Depth = Depth + 1
	end
	Depth = Depth - 2
	return Depth
end

function GetThread()
	return coroutine.running() or 'thread: Main'
end

function GetThreadDepth()
	return { Thread = GetThread(), Depth = GetDepth() - 1 }
end

function GetLocals()
	local Locals = {}
	local Depth = 3
	local I = 1
	local Name
	local Value
	while true do
		Name, Value = debug.getlocal(Depth, I)
		if Name == nil then
			break
		elseif Name == '(*temporary)' then
		else
			table.insert(Locals, {Name, Value})
		end
		I = I + 1
	end
	return Locals
end

function GetGlobals()
	local Globals = {}
	for k, v in pairs(G) do
		if GlobalNames[k] == nil then
			table.insert(Globals, {k, v})
		end
	end
	return Globals
end

function GetUpvalues()
	local Upvalues = {}
	local Depth = 3
	local Func = debug.getinfo(Depth, 'f').func
	local I = 1
	local Name
	local Value
	while true do
		Name, Value = debug.getupvalue(Func, I)
		if Name == nil then
			break
		elseif Name == '(*temporary)' then
		else
			table.insert(Upvalues, {Name, Value})
		end
		I = I + 1
	end
	return Upvalues
end

-- {{{ Hook
function Hook(Event, Line)
	if not Hooking then
		EndHook()
	end

	if not IsInBreak(Line) then
		if Mode == 'sr' then
			if Event == 'return' and GetThread() == ThreadDepth.Thread and GetDepth() <= ThreadDepth.Depth then
				ThreadDepth = nil
				Mode = 'si'
			end
			-- always return
			return
		elseif Mode == 'so' then
			if Event == 'line' and GetThread() == ThreadDepth.Thread and GetDepth() <= ThreadDepth.Depth then
				ThreadDepth = nil
				Mode = 'si'
				-- continue
			else
				return
			end
		elseif Mode == 'si' then
			if Event == 'line' then
				-- continue
			else
				return
			end
		else
			return
		end
	end

	if IsInDebug() then
		return
	end

	Mode = 'si'

	-- Do Debug

	local Info = {}
	Info.Info = debug.getinfo('2')
	Info.Thread = coroutine.running() or 'thread: Main'


	while true do
		local Op = WaitOp(Info)
		if not Op then
			return
		end
		if not Op.Mode then
			Op.Mode = 'o'
		end

		if Op.Mode == 'o' then
			if Op.Op == 'si' then
				Mode = 'si' -- step into
			elseif Op.Op == 'so' then
				Mode = 'so' -- step over
				ThreadDepth = GetThreadDepth()
			elseif Op.Op == 'sr' then
				Mode = 'sr' -- step return
				ThreadDepth = GetThreadDepth()
			elseif Op.Op == 'rm' then
				Mode = 'rm' -- resume
			elseif Op.Op == 'end' then
				sock:close()
				os.exit(1) -- terminate
			elseif Op.Op == 'get' then
				--
			elseif Op.Op == 'set' then
				--
			else
				--
			end

			if Op.BreakPoints then
				assert( type(Op.BreakPoints) == 'table' )
				BreakPoints = Op.BreakPoints
			end
			break
		elseif Op.Mode == 'r' then
			Info = {}
			if Op.Op == 'g' then
				Info.Globals = GetGlobals()
			elseif Op.Op == 'u' then
				Info.Upvalues = GetUpvalues()
			elseif Op.Op == 'l' then
				Info.Locals = GetLocals()
			end
		elseif Op.Mode == 'w' then
			print "[debug] write"
		else
		end
	end

end
-- }}}

local Coroutine = { create = coroutine.create, wrap = coroutine.wrap }

function CreateCoroutine(F)
	local Thread = Coroutine.create(F)
	debug.sethook(Thread, Hook, 'rl')
	return Thread
end

function WrapCoroutine(F)
	local Thread = Coroutine.create(F)
	debug.sethook(Thread, Hook, 'rl')
	return function(...)
		return coroutine.resume(Thread, ...)
	end
end

function InitGlobalNames()
	for n, v in pairs(G) do
		GlobalNames[n] = v
	end
end

function BeginHook()
	coroutine.create = CreateCoroutine
	coroutine.wrap = WrapCoroutine

	io.stderr:write('Waiting for connection...\n')
	WaitRemote()
	io.stderr:write('Connected.\n')
	io.stderr:write('Protocol: ', Protocol, '\n\n')

	InitGlobalNames()

	Hooking = true
	debug.sethook(Hook, "rl")
end

function EndHook()
	debug.sethook()
	Hooking = false
end
-- }}}

-- {{{ Remote
function WaitRemote()
	local socket = require("socket")
	local server = socket.bind("*", 8173)
	local client = server:accept()
	sock = client
	local Str, ErrMsg = sock:receive('*l')


	local Code, Object = pcall(Json.Decode, Str)
	if Code and type(Object) == 'string' then
		Str = Object
	end
	if string.lower(Str) == 'json' then
		Protocol = 'json'
	elseif string.lower(Str) == 'simple' then
		Protocol = 'simple'
	else
		Protocol = 'simple' -- by default
	end
end

-- {{{ Parse Object
function DeepCopy(Src, Seen)
	if type(Src) == 'function' then
		return tostring(Src)
	elseif type(Src) == 'thread' then
		return tostring(Src)
	elseif type(Src) == 'userdata' then
		return tostring(Src)
	elseif type(Src) ~= 'table' then
		return Src
	end

	if not Seen then
		Seen = {}
	end
	if Seen[Src] then
		-- TODO: support recursive table
		--error("Don't allow recursive table...")
		return tostring(Src)
	end
	local Dest = {}
	Seen[Src] = Dest
	for Key, Val in pairs(Src) do
		Key = DeepCopy(Key, Seen)
		Val = DeepCopy(Val, Seen)
		Dest[Key] = Val
	end
	return Dest
end

local Parsers = {
	json = {
		Encode = function(Object)
			Object = DeepCopy(Object)
			return Json.Encode(Object) .. '\n'
		end
		,
		Decode = function(Str)
			local Code, Object = pcall(Json.Decode, Str)
			if Code then
				return Object
			else
				print("[debug][error][parse] "..Object)
				-- use simple protocol
				return Str
			end
		end
	},
	simple = {
		Encode = function(Object)
			return Object.Info.short_src .. ': ' .. Object.Info.currentline .. '\n'
		end
		,
		Decode = function(Str)
			local File, Line = string.match(Str, '^(.*):%s*(%d+)$')
			return { Mode = 'o', Op = Str}
		end
	}
}

function SendObject(Sock, Object)
	local Str = Parsers[Protocol].Encode(Object)
	Sock:send(Str)
end

function ReceiveObject(Sock)
	local Str, ErrMsg = Sock:receive('*l')
	if ErrMsg then
		--error(ErrMsg)
		print("[debug][error][receive] "..ErrMsg)
		EndHook()
		return
	end
	if Str then
		return Parsers[Protocol].Decode(Str)
	end
end
-- }}}


function WaitOp(Info)
	--local Info = 'Connected...'
	local Op
	local ErrMsg
	while Hooking do
		SendObject(sock, Info)
		Op = ReceiveObject(sock)
		Info = coroutine.yield(Op)
	end
end

WaitOp = coroutine.wrap(WaitOp)
-- }}}

function RunScript(ScriptName, ...)
	BeginHook()
	local F, ErrMsg = loadfile(ScriptName)
	if not F then
		error(ErrMsg)
	end
	F(...)
	EndHook()
end


if(...) then
	RunScript(unpack({...}, 1, 1), select(2, ...))
else
end

-- vim: foldmethod=marker:
